#include "../gl/Window.hpp"
#include "../gl/Camera.hpp"
#include "../gl/Sphere.hpp"
#include "../opengl/opengl.hpp"
#include <vector>
#include <fmt/format.h>
#include <glm/glm.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtc/matrix_transform.hpp>

namespace gl = opengl;
PerspectiveCamera camera;

class Object3D {
public:
    Object3D() {}
    Object3D(Object3D &&other) : position(std::move(other.position)) {}
    Object3D(const Object3D &) = delete;
    Object3D &operator=(Object3D &&) = delete;
    Object3D &operator=(const Object3D &) = delete;

public:
    void updateMatrix() {
        modelMatrix = glm::scale(glm::mat4(1.0f), scale);
        modelMatrix = glm::eulerAngleXYZ(rotation.x, rotation.y, rotation.z) * modelMatrix;
        modelMatrix = glm::translate(modelMatrix, position);
        normalMatrix = glm::mat3(glm::transpose(glm::inverse(modelMatrix)));
    }

public:
    glm::vec3 position{0.0f};
    glm::vec3 scale{1.0f};
    glm::vec3 rotation{0.0f};

public:
    glm::mat4 modelMatrix{1.0f};
    glm::mat3 normalMatrix{1.0f};
};

class Mesh3D : public Object3D {
public:
    struct VertexData {
        std::vector<Vertex> vertices;
        std::vector<gl::u32> indices{};
    };
    struct Material {
        glm::vec3 color{1.0f};
        float shininess{32.0f};
    };

public:
    Mesh3D() {}
    Mesh3D(const VertexData &data, const char *vsFile, const char *fsFile) { create(data, vsFile, fsFile); };
    void create(const VertexData &data, const char *vsFile, const char *fsFile) {
        vao.create({gl::vertex_attribute(0, gl::type_f32, 3, 0), gl::vertex_attribute(1, gl::type_f32, 3, 12)});
        vbo.create(sizeof(Vertex) * data.vertices.size(), data.vertices.data());
        numIndices = data.indices.size();
        ibo.create(4 * numIndices, data.indices.data());
        program.create({gl::shader::from_file(gl::shader::vertex_shader, vsFile),
                        gl::shader::from_file(gl::shader::fragment_shader, fsFile)});
        vao.set_vertex_buffer(vbo, sizeof(Vertex));
        vao.set_index_buffer(ibo);
    };
    void updateRenderData(gl::program &pr) {
        updateMatrix();
        pr.set_vec3f(pr.get_uniform_location("cameraPos"), glm::value_ptr(camera.position));
        pr.set_mat4f(pr.get_uniform_location("modelMatrix"), glm::value_ptr(modelMatrix));
        pr.set_mat3f(pr.get_uniform_location("normalMatrix"), glm::value_ptr(normalMatrix));
        pr.set_mat4f(pr.get_uniform_location("viewProjectionMatrix"), glm::value_ptr(camera.viewProjectionMatrix));
        pr.set_vec3f(pr.get_uniform_location("material.color"), glm::value_ptr(material.color));
        pr.set_float32(pr.get_uniform_location("material.shininess"), material.shininess);
    }
    void render(gl::program &pr) {
        updateRenderData(pr);
        vao.bind();
        pr.use();
        gl::api::glDrawElements(gl::mode_triangles, numIndices, gl::type_u32, nullptr);
    }
    void render() {
        updateMatrix();
        program.set_vec3f(program.get_uniform_location("cameraPos"), glm::value_ptr(camera.position));
        program.set_mat4f(program.get_uniform_location("modelMatrix"), glm::value_ptr(modelMatrix));
        program.set_mat3f(program.get_uniform_location("normalMatrix"), glm::value_ptr(normalMatrix));
        program.set_mat4f(program.get_uniform_location("viewProjectionMatrix"),
                          glm::value_ptr(camera.viewProjectionMatrix));
        program.set_vec3f(program.get_uniform_location("material.color"), glm::value_ptr(material.color));
        program.set_float32(program.get_uniform_location("material.shininess"), material.shininess);
        vao.bind();
        program.use();
        gl::api::glDrawElements(gl::mode_triangles, numIndices, gl::type_u32, nullptr);
    }

public:
    Material material{};
    gl::vertex_array vao{};
    gl::buffer vbo{}, ibo{};
    gl::program program{};
    gl::u32 numIndices{0};
};

int main() {
    Window window("Model", 800, 600);
    gl::init_opengl(gl::opengl_config{.load_function = &SDL_GL_GetProcAddress,
                                      .debug_msg_callback = [](auto, auto, auto id, auto, auto, auto msg, auto) {
                                          fmt::println("[OpenGL object {}] {}", id, msg);
                                      }});
    camera.set_position({6.0f, 10.0f, -6.0f});
    camera.look_at(glm::vec3{0.0f});
    window.on_exec = [&](auto) {
        auto [w, h] = window.get_size();
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
        gl::api::glEnable(gl::stencil_test);
    };
    window.on_resize = [&](auto, int w, int h) {
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };
    window.on_process_event = [&](auto, const SDL_Event *event) {
        if (event->type == SDL_EventType::SDL_MOUSEWHEEL) {
            constexpr float sensitivity = 2.0f;
            auto fov = glm::clamp(camera.fov - sensitivity * event->wheel.preciseY, 15.0f, 85.0f);
            camera.set_fov(fov);
        }
    };
    Mesh3D plane(Mesh3D::VertexData{.vertices =
                                        {
                                            Vertex{{1, 0, 1}, {0, 1, 0}},
                                            Vertex{{1, 0, -1}, {0, 1, 0}},
                                            Vertex{{-1, 0, -1}, {0, 1, 0}},
                                            Vertex{{-1, 0, 1}, {0, 1, 0}},
                                        },
                                    .indices = {0, 1, 2, 0, 2, 3}},
                 "../shader/stencil/vs.glsl", "../shader/stencil/fs.glsl");
    ;
    plane.material.color = {1.0f, 0.8431f, 0.f};
    plane.material.shininess = 1.0f;
    Mesh3D sphere1, sphere2;
    {
        Sphere<64> data;
        sphere1.create(Mesh3D::VertexData{.vertices{data.vertices, data.vertices + data.num_vertices},
                                          .indices{data.indices, data.indices + data.num_indices}},
                       "../shader/stencil/vs.glsl", "../shader/stencil/fs.glsl");
        sphere2.create(Mesh3D::VertexData{.vertices{data.vertices, data.vertices + data.num_vertices},
                                          .indices{data.indices, data.indices + data.num_indices}},
                       "../shader/stencil/vs.glsl", "../shader/stencil/fs.glsl");
        sphere1.material.shininess = 2.0f;
        sphere1.material.color = {0.f, 1.f, 0.f};
        sphere1.position = {0.f, 2.f, 0.f};
        sphere2.material.shininess = 2.0f;
        sphere2.material.color = {1.f, 0.f, 0.f};
        sphere2.position = {1.f, 3.f, 0.f};
    }

    gl::buffer lightBuffer;
    struct {
        alignas(16) glm::vec3 ambient{0.3f};
        alignas(16) glm::vec3 diffuse{0.6f};
        alignas(16) glm::vec3 specular{0.1f};
        alignas(16) glm::vec3 position{3.f, 7.f, 3.f};
    } light;
    lightBuffer.create(sizeof(light), &light);
    lightBuffer.bind_to(gl::buffer::uniform_buffer, 0);

    gl::program normalProgram({
        gl::shader::from_file(gl::shader::vertex_shader, "../shader/stencil/vs.glsl"),
        gl::shader::from_file(gl::shader::fragment_shader, "../shader/stencil/fs.glsl"),
    });
    gl::program outlineProgram({
        gl::shader::from_file(gl::shader::vertex_shader, "../shader/stencil/ovs.glsl"),
        gl::shader::from_file(gl::shader::fragment_shader, "../shader/stencil/ofs.glsl"),
    });
    fmt::println("{}\n\n\n{}", normalProgram.get_error(), outlineProgram.get_error());

    // window.on_render = [&](auto) {
    //     gl::api::glClear(gl::OPENGL_COLOR_BUFFER_BIT | gl::OPENGL_DEPTH_BUFFER_BIT | gl::OPENGL_STENCIL_BUFFER_BIT);
    //     gl::api::glClearColor(0, 0, 0, 1);
    //     camera.update_matrix();

    //     gl::api::glEnable(gl::depth_test);
    //     gl::api::glStencilMask(0x00);
    //     plane.render();

    //     auto drawOutlined = [&](Mesh3D &mesh) {
    //         gl::api::glEnable(gl::depth_test);
    //         gl::api::glClear(gl::OPENGL_STENCIL_BUFFER_BIT);
    //         gl::api::glStencilOp(0x1e00, 0x1e00, 0x1e01);
    //         gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
    //         gl::api::glStencilMask(0xFF);
    //         normalProgram.use();
    //         mesh.updateRenderData(normalProgram);
    //         mesh.vao.bind();
    //         gl::api::glDrawElements(gl::mode_triangles, mesh.numIndices, gl::type_u32, nullptr);
    //         gl::api::glStencilFunc(gl::cmp_notequal, 1, 0xFF);
    //         gl::api::glStencilMask(0x00);
    //         gl::api::glDisable(gl::depth_test);
    //         outlineProgram.use();
    //         mesh.updateRenderData(outlineProgram);
    //         gl::api::glDrawElements(gl::mode_triangles, mesh.numIndices, gl::type_u32, nullptr);
    //     };
    //     drawOutlined(sphere1);
    //     drawOutlined(sphere2);

    //     gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
    //     gl::api::glStencilMask(0xFF);
    //     gl::api::glEnable(gl::depth_test);
    // };

    window.on_render = [&](auto dt) {
        gl::api::glClear(gl::OPENGL_COLOR_BUFFER_BIT | gl::OPENGL_DEPTH_BUFFER_BIT | gl::OPENGL_STENCIL_BUFFER_BIT);
        gl::api::glClearColor(0, 0, 0, 1);
        camera.update_matrix();
        plane.scale = {5, 1, 5};
        gl::api::glStencilMask(0x00);
        plane.render();

        gl::api::glEnable(gl::depth_test);
        gl::api::glStencilMask(0xFF);
        gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
        gl::api::glStencilOp(0x1e00, 0x1e00, 0x1e01);
        sphere1.render(normalProgram);
        gl::api::glStencilFunc(gl::cmp_notequal, 1, 0xFF);
        gl::api::glStencilMask(0x00);
        gl::api::glDisable(gl::depth_test);
        sphere1.render(outlineProgram);
        gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
        gl::api::glStencilMask(0xFF);

        gl::api::glEnable(gl::depth_test);
        gl::api::glStencilMask(0xFF);
        gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
        gl::api::glStencilOp(0x1e00, 0x1e00, 0x1e01);
        sphere2.render(normalProgram);
        gl::api::glStencilFunc(gl::cmp_notequal, 1, 0xFF);
        gl::api::glStencilMask(0x00);
        gl::api::glDisable(gl::depth_test);
        sphere2.render(outlineProgram);
        gl::api::glStencilFunc(gl::cmp_always, 1, 0xFF);
        gl::api::glStencilMask(0xFF);
    };

    return window.exec();
}